// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight.daemon.problems

import com.intellij.codeInsight.daemon.problems.Problem
import com.intellij.codeInsight.daemon.problems.pass.ProjectProblemUtils
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.command.undo.UndoManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.refactoring.RefactoringFactory
import com.intellij.refactoring.move.moveInner.MoveInnerImpl
import com.intellij.refactoring.openapi.impl.MoveInnerRefactoringImpl

internal class ClassProblemsTest : ProjectProblemsViewTest() {
  fun testRename() = doClassTest { psiClass, factory ->
    psiClass.nameIdentifier?.replace(factory.createIdentifier("Bar"))
  }

  fun testMakeClassFinal() = doClassTest { psiClass, _ ->
    psiClass.modifierList?.setModifierProperty(PsiModifier.FINAL, true)
  }

  fun testMakeClassPackagePrivate() = doClassTest { psiClass, _ ->
    psiClass.modifierList?.setModifierProperty(PsiModifier.PUBLIC, false)
  }

  fun testChangeClassHierarchy() = doClassTest { psiClass, factory ->
    psiClass.extendsList?.replace(factory.createReferenceList(PsiJavaCodeReferenceElement.EMPTY_ARRAY))
  }

  fun testSealedClassPermittedInheritors() {
    val targetClass = myFixture.addClass("""
        package foo;
        public sealed class A {
        }
      """.trimIndent())
    val refClass = myFixture.addClass("""
        package foo;
        
        public class B extends A {
        }
      """.trimIndent())

    doTest(targetClass) {
      changeClass(targetClass) { psiClass, _ ->
        val factory = PsiFileFactory.getInstance(project)
        val javaFile = factory.createFileFromText(JavaLanguage.INSTANCE, "class __Dummy permits B {}") as PsiJavaFile
        val dummyClass = javaFile.classes[0]
        val permitsList = dummyClass.permitsList
        psiClass.addAfter(permitsList!!, psiClass.implementsList)
      }
      assertTrue(hasReportedProblems<PsiClass>(refClass))
      myFixture.openFileInEditor(refClass.containingFile.virtualFile)
      changeClass(refClass) { psiClass, _ ->
        psiClass.modifierList?.setModifierProperty(PsiModifier.FINAL, true)
      }
      myFixture.openFileInEditor(targetClass.containingFile.virtualFile)
      myFixture.doHighlighting()
      assertFalse(hasReportedProblems<PsiClass>(refClass))
    }
  }
  
  fun testClassOverrideBecamePrivate() {
    myFixture.addClass("""
        package foo;
        
        public abstract class Parent {
          abstract void test();
        }
    """.trimIndent())
    val targetClass = myFixture.addClass("""
        package foo;
        
        public class Foo extends Parent {
          void test() {}
        }
    """.trimIndent())
    val refClass = myFixture.addClass("""
        package foo;
        
        public class Usage {
          void use() {
            Foo foo = new Foo();
            foo.test();
          }
        }
    """.trimIndent())
    
    doTest(targetClass) {
      changeClass(targetClass) { psiClass, factory -> 
        psiClass.methods[0].parameterList.add(factory.createParameterFromText("int a", psiClass))
      }

      changeClass(targetClass) { psiClass, _ ->
        psiClass.methods[0].modifierList.setModifierProperty(PsiModifier.PUBLIC, true)
      }
      
      changeClass(targetClass) { psiClass, factory ->
        psiClass.extendsList?.replace(factory.createReferenceList(PsiJavaCodeReferenceElement.EMPTY_ARRAY))
      }
    }
    
    assertTrue(hasReportedProblems<PsiMethod>(refClass))
  }

  fun testMakeClassInterface() {
    val targetClass = myFixture.addClass("""
        package foo;
        public class A {
        }
      """.trimIndent())
    val refClass = myFixture.addClass("""
        package bar;
        import foo.*;
        
        public class B {
          void test() { 
            A a = new A();;
          }
        }
      """.trimIndent())
    doTest(targetClass) {
      changeClass(targetClass) { psiClass, factory ->
        val classKeyword = PsiTreeUtil.getPrevSiblingOfType(psiClass.nameIdentifier, PsiKeyword::class.java)
        val interfaceKeyword = factory.createKeyword(PsiKeyword.INTERFACE)
        classKeyword?.replace(interfaceKeyword)
      }
      assertTrue(hasReportedProblems<PsiDeclarationStatement>(refClass))
    }
  }

  fun testMakeNestedInner() {
    doNestedClassTest(true)
  }

  fun testMakeInnerNested() {
    doNestedClassTest(false)
  }

  fun testMakeClassAnnotationType() {
    var targetClass = myFixture.addClass("""
      package foo;
      
      public class A {
        
      }
    """.trimIndent())

    val refClass = myFixture.addClass("""
      package bar;
      
      import foo.*;
      
      public class B {
      
        void test() { 
          A a = new A();
        }
      }
    """.trimIndent())

    doTest(targetClass) {
      changeClass(targetClass) { _, factory ->
        val annoType = factory.createAnnotationType("A")
        targetClass = targetClass.replace(annoType) as PsiClass
      }
    }
    assertTrue(hasReportedProblems<PsiDeclarationStatement>(refClass))
  }

  fun testInheritedMethodUsage() {
    myFixture.addClass("""
        package foo;
        
        public class Parent {
          public void foo() {}
        }
      """.trimIndent())

    val aClass = myFixture.addClass("""
        package foo;
        
        public class A extends Parent {
        }
      """.trimIndent())

    val refClass = myFixture.addClass("""
        package foo;
        
        public class Usage {
          void test() {
            (new A()).foo();
          }
        }
    """.trimIndent())

    doTest(aClass) {
      changeClass(aClass) { psiClass, factory ->
        psiClass.extendsList?.replace(factory.createReferenceList(PsiJavaCodeReferenceElement.EMPTY_ARRAY))
      }

      assertTrue(hasReportedProblems<PsiClass>(refClass))
    }
  }

  fun testMoveInnerClassAndUndo() {
    val refClass = myFixture.addClass("""
        public class A {
          private String s = "foo";
          public class Inner {
            void test() {
              System.out.println(s);
            }
          }
        }
    """.trimIndent())

    myFixture.openFileInEditor(refClass.containingFile.virtualFile)

    doTest(refClass) {
      changeClass(refClass) { psiClass, _ ->
        val innerClass = psiClass.innerClasses[0]
        val targetContainer = MoveInnerImpl.getTargetContainer(innerClass, false)!!
        val moveRefactoring = MoveInnerRefactoringImpl(myFixture.project, innerClass, innerClass.name, true, "a", targetContainer)
        BaseRefactoringProcessor.ConflictsInTestsException.withIgnoredConflicts<Throwable> { moveRefactoring.run() }
      }
      changeClass(refClass) { psiClass, _ ->
        psiClass.fields[0].modifierList?.setModifierProperty(PsiModifier.PRIVATE, false)
      }
      changeClass(refClass) { psiClass, _ ->
        psiClass.fields[0].modifierList?.setModifierProperty(PsiModifier.PRIVATE, true)
      }

      assertNotEmpty(getProblems())

      val selectedEditor = FileEditorManager.getInstance(project).selectedEditor
      WriteCommandAction.runWriteCommandAction(project) {
        UndoManager.getInstance(project).undo(selectedEditor)
        UndoManager.getInstance(project).undo(selectedEditor)
        UndoManager.getInstance(project).undo(selectedEditor)
      }
      PsiDocumentManager.getInstance(project).commitAllDocuments()

      myFixture.doHighlighting()
      assertEmpty(ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)
    }
  }

  fun testRenameClassRenameMethodAndUndoAll() {
    val targetClass = myFixture.addClass("""
        public class A {
          public void foo() {}
          
          public void bar() {}
        }
    """.trimIndent())

    myFixture.addClass("""
        public class RefClass {
          void test() {
            A a = new A();
            a.foo();
            a.bar();
          }
        }
    """.trimIndent())

    doTest(targetClass) {
      val factory = JavaPsiFacade.getInstance(project).elementFactory

      changeClass(targetClass) { psiClass, _ ->
        psiClass.identifyingElement?.replace(factory.createIdentifier("A1"))
      }

      val problems: Map<PsiMember, Set<Problem>> = ProjectProblemUtils.getReportedProblems(myFixture.editor)
      val reportedMembers = problems.map { it.key }
      assertSize(1, reportedMembers)
      assertTrue(targetClass in reportedMembers)

      WriteCommandAction.runWriteCommandAction(project) {
        val method = targetClass.findMethodsByName("foo", false)[0]
        method.identifyingElement?.replace(factory.createIdentifier("foo1"))
      }
      myFixture.doHighlighting()
      assertNotEmpty(ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)

      WriteCommandAction.runWriteCommandAction(project) {
        val method = targetClass.findMethodsByName("foo1", false)[0]
        method.identifyingElement?.replace(factory.createIdentifier("foo"))
      }
      myFixture.doHighlighting()
      assertSize(1, ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)

      changeClass(targetClass) { psiClass, _ ->
        psiClass.identifyingElement?.replace(factory.createIdentifier("A"))
      }

      assertEmpty(ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)
    }
  }

  fun testAddMissingTypeParam() {
    val targetClass = myFixture.addClass("""
      class TargetClass {
      }
    """.trimIndent())

    myFixture.addClass("""
      public class RefClass extends TargetClass<T> {
      }
    """.trimIndent())

    doTest(targetClass) {
      changeClass(targetClass) { psiClass, _ ->
        psiClass.modifierList?.setModifierProperty(PsiModifier.PUBLIC, true)
      }

      assertSize(1, ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)

      changeClass(targetClass) { psiClass, factory ->
        val typeParameterList = factory.createTypeParameterList()
        typeParameterList.add(factory.createTypeParameterFromText("T", psiClass))
        psiClass.typeParameterList?.replace(typeParameterList)
      }

      assertEmpty(ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)
    }
  }

  fun testRenameClassAndFixUsages() {
    val targetClass = myFixture.addClass("""
      class AClass {
      }
    """.trimIndent())

    myFixture.addClass("""
      class BClass {
        AClass tmp;

        public void method1(String arg) {
            System.out.println(tmp.toString());
        }
      }
    """.trimIndent())

    doTest(targetClass) {
      changeClass(targetClass) { psiClass, factory ->
        psiClass.nameIdentifier?.replace(factory.createIdentifier("AClass1"))
      }

      assertSize(1, ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)

      changeClass(targetClass) { psiClass, _ ->
        psiClass.setName("AClass")
        val renameRefactoring = RefactoringFactory.getInstance(project).createRename(psiClass, "AClass1", true, false)
        renameRefactoring.run()
      }

      assertEmpty(ProjectProblemUtils.getReportedProblems(myFixture.editor).entries)
    }
  }

  private fun doNestedClassTest(isStatic: Boolean) {
    val staticModifier = if (isStatic) "static" else ""
    val targetClass = myFixture.addClass("""
        package foo;
        
        public class A {
          public $staticModifier class Inner {}
        }
      """.trimIndent())

    val initializer = if (isStatic) "new A.Inner();" else "new A().new Inner()"
    val refClass = myFixture.addClass("""
        package bar;
        
        import foo.*;
        
        public class B {
        
          void test() { 
            A.Inner aInner = $initializer;
          }
        }
      """.trimIndent())

    doTest(targetClass) {
      changeClass(targetClass) { psiClass, _ ->
        val innerClass = psiClass.findInnerClassByName("Inner", false)
        innerClass?.modifierList?.setModifierProperty(PsiModifier.STATIC, !isStatic)
      }
      assertTrue(hasReportedProblems<PsiDeclarationStatement>(refClass))
    }
  }

  private fun doClassTest(classChangeAction: (PsiClass, PsiElementFactory) -> Unit) {

    myFixture.addClass("""
      package foo;
      public class Parent {
      }
    """.trimIndent())


    val targetClass = myFixture.addClass("""
      package foo;
      
      public class A extends Parent {
        
      }
    """.trimIndent())

    val refClass = myFixture.addClass("""
      package bar;
      
      import foo.*;
      
      public class B {
      
        void test() { 
          Parent parent = new A() {};
        }
      }
    """.trimIndent())

    doTest(targetClass) {
      changeClass(targetClass, classChangeAction)
      assertTrue(hasReportedProblems<PsiDeclarationStatement>(refClass))
    }
  }

  private fun changeClass(targetClass: PsiClass, classChangeAction: (PsiClass, PsiElementFactory) -> Unit) {
    WriteCommandAction.runWriteCommandAction(project) {
      val factory = JavaPsiFacade.getInstance(project).elementFactory
      classChangeAction(targetClass, factory)
    }
    myFixture.doHighlighting()
  }
}
